/* 
Copyright Paul James Mutton, 2001-2007, http://www.jibble.org/

This file is part of PircBot.

This software is dual-licensed, allowing you to choose between the GNU
General Public License (GPL) and the www.jibble.org Commercial License.
Since the GPL may be too restrictive for use in a proprietary application,
a commercial license is also provided. Full license information can be
found at http://www.jibble.org/licenses/

 */

package org.jibble.pircbot;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * This class is used to administer a DCC file transfer.
 * 
 * @since 1.2.0
 * @author Paul James Mutton, <a
 *         href="http://www.jibble.org/">http://www.jibble.org/</a>
 * @version 1.4.6 (Build time: Wed Apr 11 19:20:59 2007)
 */
public class DccFileTransfer {

	/**
	 * The default buffer size to use when sending and receiving files.
	 */
	public static final int BUFFER_SIZE = 1024;

	private PircBot _bot;

	private DccManager _manager;

	private String _nick;

	private String _login = null;

	private String _hostname = null;

	private String _type;

	private long _address;

	private int _port;

	private long _size;

	private boolean _received;

	private Socket _socket = null;

	private long _progress = 0;

	private File _file = null;

	private int _timeout = 0;

	private boolean _incoming;

	private long _packetDelay = 0;

	private long _startTime = 0;

	/**
	 * Constructor used for sending files.
	 */
	DccFileTransfer(PircBot bot, DccManager manager, File file, String nick,
			int timeout) {
		_bot = bot;
		_manager = manager;
		_nick = nick;
		_file = file;
		_size = file.length();
		_timeout = timeout;
		_received = true;

		_incoming = false;
	}

	/**
	 * Constructor used for receiving files.
	 */
	DccFileTransfer(PircBot bot, DccManager manager, String nick, String login,
			String hostname, String type, String filename, long address,
			int port, long size) {
		_bot = bot;
		_manager = manager;
		_nick = nick;
		_login = login;
		_hostname = hostname;
		_type = type;
		_file = new File(filename);
		_address = address;
		_port = port;
		_size = size;
		_received = false;

		_incoming = true;
	}

	/**
	 * Stops the DCC file transfer by closing the connection.
	 */
	public void close() {
		try {
			_socket.close();
		} catch (Exception e) {
			// Let the DCC manager worry about anything that may go wrong.
		}
	}

	/**
	 * Delay between packets.
	 */
	private void delay() {
		if (_packetDelay > 0) {
			try {
				Thread.sleep(_packetDelay);
			} catch (InterruptedException e) {
				// Do nothing.
			}
		}
	}

	/**
	 * Receive the file in a new thread.
	 */
	void doReceive(final File file, final boolean resume) {
		new Thread() {
			@Override
			public void run() {

				BufferedOutputStream foutput = null;
				Exception exception = null;

				try {

					// Convert the integer address to a proper IP address.
					int[] ip = _bot.longToIp(_address);
					String ipStr = ip[0] + "." + ip[1] + "." + ip[2] + "."
							+ ip[3];

					// Connect the socket and set a timeout.
					_socket = new Socket(ipStr, _port);
					_socket.setSoTimeout(30 * 1000);
					_startTime = System.currentTimeMillis();

					// No longer possible to resume this transfer once it's
					// underway.
					_manager.removeAwaitingResume(DccFileTransfer.this);

					BufferedInputStream input = new BufferedInputStream(
							_socket.getInputStream());
					BufferedOutputStream output = new BufferedOutputStream(
							_socket.getOutputStream());

					// Following line fixed for jdk 1.1 compatibility.
					foutput = new BufferedOutputStream(new FileOutputStream(
							file.getCanonicalPath(), resume));

					byte[] inBuffer = new byte[BUFFER_SIZE];
					byte[] outBuffer = new byte[4];
					int bytesRead = 0;
					while ((bytesRead = input
							.read(inBuffer, 0, inBuffer.length)) != -1) {
						foutput.write(inBuffer, 0, bytesRead);
						_progress += bytesRead;
						// Send back an acknowledgement of how many bytes we
						// have got so far.
						outBuffer[0] = (byte) ((_progress >> 24) & 0xff);
						outBuffer[1] = (byte) ((_progress >> 16) & 0xff);
						outBuffer[2] = (byte) ((_progress >> 8) & 0xff);
						outBuffer[3] = (byte) ((_progress >> 0) & 0xff);
						output.write(outBuffer);
						output.flush();
						delay();
					}
					foutput.flush();
				} catch (Exception e) {
					exception = e;
				} finally {
					try {
						if (foutput != null) {
							foutput.close();
						}
						_socket.close();
					} catch (Exception anye) {
						// Do nothing.
					}
				}

				_bot.onFileTransferFinished(DccFileTransfer.this, exception);
			}
		}.start();
	}

	/**
	 * Method to send the file inside a new thread.
	 */
	void doSend(final boolean allowResume) {
		new Thread() {
			@Override
			public void run() {

				BufferedInputStream finput = null;
				Exception exception = null;

				try {

					ServerSocket ss = null;

					int[] ports = _bot.getDccPorts();
					if (ports == null) {
						// Use any free port.
						ss = new ServerSocket(0);
					} else {
						for (int i = 0; i < ports.length; i++) {
							try {
								ss = new ServerSocket(ports[i]);
								// Found a port number we could use.
								break;
							} catch (Exception e) {
								// Do nothing; go round and try another port.
							}
						}
						if (ss == null) {
							// No ports could be used.
							throw new IOException(
									"All ports returned by getDccPorts() are in use.");
						}
					}

					ss.setSoTimeout(_timeout);
					_port = ss.getLocalPort();
					InetAddress inetAddress = _bot.getDccInetAddress();
					if (inetAddress == null) {
						inetAddress = _bot.getInetAddress();
					}
					byte[] ip = inetAddress.getAddress();
					long ipNum = _bot.ipToLong(ip);

					// Rename the filename so it has no whitespace in it when we
					// send it.
					// .... I really should do this a bit more nicely at some
					// point ....
					String safeFilename = _file.getName().replace(' ', '_');
					safeFilename = safeFilename.replace('\t', '_');

					if (allowResume) {
						_manager.addAwaitingResume(DccFileTransfer.this);
					}

					// Send the message to the user, telling them where to
					// connect to in order to get the file.
					_bot.sendCTCPCommand(_nick, "DCC SEND " + safeFilename
							+ " " + ipNum + " " + _port + " " + _file.length());

					// The client may now connect to us and download the file.
					_socket = ss.accept();
					_socket.setSoTimeout(30000);
					_startTime = System.currentTimeMillis();

					// No longer possible to resume this transfer once it's
					// underway.
					if (allowResume) {
						_manager.removeAwaitingResume(DccFileTransfer.this);
					}

					// Might as well close the server socket now; it's finished
					// with.
					ss.close();

					BufferedOutputStream output = new BufferedOutputStream(
							_socket.getOutputStream());
					BufferedInputStream input = new BufferedInputStream(
							_socket.getInputStream());
					finput = new BufferedInputStream(new FileInputStream(_file));

					// Check for resuming.
					if (_progress > 0) {
						long bytesSkipped = 0;
						while (bytesSkipped < _progress) {
							bytesSkipped += finput.skip(_progress
									- bytesSkipped);
						}
					}

					byte[] outBuffer = new byte[BUFFER_SIZE];
					byte[] inBuffer = new byte[4];
					int bytesRead = 0;
					while ((bytesRead = finput.read(outBuffer, 0,
							outBuffer.length)) != -1) {
						output.write(outBuffer, 0, bytesRead);
						output.flush();
						input.read(inBuffer, 0, inBuffer.length);
						_progress += bytesRead;
						delay();
					}
				} catch (Exception e) {
					exception = e;
				} finally {
					try {
						if (finput != null) {
							finput.close();
						}
						_socket.close();
					} catch (Exception e) {
						// Do nothing.
					}
				}

				_bot.onFileTransferFinished(DccFileTransfer.this, exception);
			}
		}.start();
	}

	/**
	 * Returns the suggested file to be used for this transfer.
	 * 
	 * @return the suggested file to be used.
	 * 
	 */
	public File getFile() {
		return _file;
	}

	/**
	 * Returns the hostname of the file sender.
	 * 
	 * @return the hostname of the file sender. null if we are sending.
	 * 
	 */
	public String getHostname() {
		return _hostname;
	}

	/**
	 * Returns the login of the file sender.
	 * 
	 * @return the login of the file sender. null if we are sending.
	 * 
	 */
	public String getLogin() {
		return _login;
	}

	/**
	 * Returns the nick of the other user taking part in this file transfer.
	 * 
	 * @return the nick of the other user.
	 * 
	 */
	public String getNick() {
		return _nick;
	}

	/**
	 * Returns the address of the sender as a long.
	 * 
	 * @return the address of the sender as a long.
	 */
	public long getNumericalAddress() {
		return _address;
	}

	/**
	 * returns the delay time between each packet that is send or received.
	 * 
	 * @return the delay between each packet.
	 * 
	 */
	public long getPacketDelay() {
		return _packetDelay;
	}

	/**
	 * Returns the port number to be used when making the connection.
	 * 
	 * @return the port number.
	 * 
	 */
	public int getPort() {
		return _port;
	}

	/**
	 * Returns the progress (in bytes) of the current file transfer. When
	 * resuming, this represents the total number of bytes in the file, which
	 * may be greater than the amount of bytes resumed in just this transfer.
	 * 
	 * @return the progress of the transfer.
	 */
	public long getProgress() {
		return _progress;
	}

	/**
	 * Returns the progress of the file transfer as a percentage. Note that this
	 * should never be negative, but could become greater than 100% if you
	 * attempt to resume a larger file onto a partially downloaded file that was
	 * smaller.
	 * 
	 * @return the progress of the transfer as a percentage.
	 */
	public double getProgressPercentage() {
		return 100 * (getProgress() / (double) getSize());
	}

	/**
	 * Returns the size (in bytes) of the file being transfered.
	 * 
	 * @return the size of the file. Returns -1 if the sender did not specify
	 *         this value.
	 */
	public long getSize() {
		return _size;
	}

	/**
	 * Returns the rate of data transfer in bytes per second. This value is an
	 * estimate based on the number of bytes transfered since the connection was
	 * established.
	 * 
	 * @return data transfer rate in bytes per second.
	 */
	public long getTransferRate() {
		long time = (System.currentTimeMillis() - _startTime) / 1000;
		if (time <= 0) {
			return 0;
		}
		return getProgress() / time;
	}

	/**
	 * Returns true if the file transfer is incoming (somebody is sending the
	 * file to us).
	 * 
	 * @return true if the file transfer is incoming.
	 * 
	 */
	public boolean isIncoming() {
		return _incoming;
	}

	/**
	 * Returns true if the file transfer is outgoing (we are sending the file to
	 * someone).
	 * 
	 * @return true if the file transfer is outgoing.
	 * 
	 */
	public boolean isOutgoing() {
		return !isIncoming();
	}

	/**
	 * Receives a DccFileTransfer and writes it to the specified file. Resuming
	 * allows a partial download to be continue from the end of the current file
	 * contents.
	 * 
	 * @param file
	 *            The file to write to.
	 * @param resume
	 *            True if you wish to try and resume the download instead of
	 *            overwriting an existing file.
	 * 
	 */
	public synchronized void receive(File file, boolean resume) {
		if (!_received) {
			_received = true;
			_file = file;

			if (_type.equals("SEND") && resume) {
				_progress = file.length();
				if (_progress == 0) {
					doReceive(file, false);
				} else {
					_bot.sendCTCPCommand(_nick, "DCC RESUME file.ext " + _port
							+ " " + _progress);
					_manager.addAwaitingResume(this);
				}
			} else {
				_progress = file.length();
				doReceive(file, resume);
			}
		}
	}

	/**
	 * Sets the delay time between sending or receiving each packet. Default is
	 * 0. This is useful for throttling the speed of file transfers to maintain
	 * a good quality of service for other things on the machine or network.
	 * 
	 * @param millis
	 *            The number of milliseconds to wait between packets.
	 * 
	 */
	public void setPacketDelay(long millis) {
		_packetDelay = millis;
	}

	/**
	 * Package mutator for setting the progress of the file transfer.
	 */
	void setProgress(long progress) {
		_progress = progress;
	}

}
